import asyncio
import hub

from unittest.mock import Mock

from hub.herald import HUB_PROTOCOL_VERSION
from hub.herald.udp import StatusServer
from hub.herald.session import LBRYElectrumX
from hub.scribe.network import LBCRegTest

from lbry.wallet.network import Network
from lbry.wallet.orchstr8 import Conductor
from lbry.wallet.orchstr8.node import SPVNode
from lbry.wallet.rpc import RPCSession
from lbry.testcase import IntegrationTestCase, AsyncioTestCase
from lbry.conf import Config


class NetworkTests(IntegrationTestCase):

    async def test_remote_height_updated_automagically(self):
        initial_height = self.ledger.network.remote_height
        await self.blockchain.generate(1)
        await self.ledger.network.on_header.first
        self.assertEqual(self.ledger.network.remote_height, initial_height + 1)

    async def test_server_features(self):
        self.assertDictEqual({
            'genesis_hash': LBCRegTest.GENESIS_HASH,
            'hash_function': 'sha256',
            'hosts': {},
            'protocol_max': '0.199.0',
            'protocol_min': '0.54.0',
            'pruning': None,
            'description': '',
            'payment_address': '',
            'donation_address': '',
            'daily_fee': '0',
            'server_version': HUB_PROTOCOL_VERSION,
            'trending_algorithm': 'fast_ar',
            }, await self.ledger.network.get_server_features())
        # await self.conductor.spv_node.stop()
        payment_address, donation_address = await self.account.get_addresses(limit=2)

        original_address = self.conductor.spv_node.server.env.payment_address
        original_donation_address = self.conductor.spv_node.server.env.donation_address
        original_description = self.conductor.spv_node.server.env.description
        original_daily_fee = self.conductor.spv_node.server.env.daily_fee

        self.conductor.spv_node.server.env.payment_address = payment_address
        self.conductor.spv_node.server.env.donation_address = donation_address
        self.conductor.spv_node.server.env.description = 'Fastest server in the west.'
        self.conductor.spv_node.server.env.daily_fee = '42'

        LBRYElectrumX.set_server_features(self.conductor.spv_node.server.env)

        # await self.ledger.network.on_connected.first
        self.assertDictEqual({
            'genesis_hash': LBCRegTest.GENESIS_HASH,
            'hash_function': 'sha256',
            'hosts': {},
            'protocol_max': '0.199.0',
            'protocol_min': '0.54.0',
            'pruning': None,
            'description': 'Fastest server in the west.',
            'payment_address': payment_address,
            'donation_address': donation_address,
            'daily_fee': '42',
            'server_version': HUB_PROTOCOL_VERSION,
            'trending_algorithm': 'fast_ar',
            }, await self.ledger.network.get_server_features())

        # cleanup the changes since the attributes are set on the class
        self.conductor.spv_node.server.env.payment_address = original_address
        self.conductor.spv_node.server.env.donation_address = original_donation_address
        self.conductor.spv_node.server.env.description = original_description
        self.conductor.spv_node.server.env.daily_fee = original_daily_fee
        LBRYElectrumX.set_server_features(self.conductor.spv_node.server.env)


class ReconnectTests(IntegrationTestCase):

    async def test_multiple_servers(self):
        # we have a secondary node that connects later, so
        node2 = SPVNode(node_number=2)
        await node2.start(self.blockchain)

        self.ledger.network.config['explicit_servers'].append((node2.hostname, node2.port))
        self.ledger.network.config['explicit_servers'].reverse()
        self.assertEqual(50002, self.ledger.network.client.server[1])
        await self.ledger.stop()
        await self.ledger.start()

        self.assertTrue(self.ledger.network.is_connected)
        self.assertEqual(50003, self.ledger.network.client.server[1])
        await node2.stop(True)
        self.assertFalse(self.ledger.network.is_connected)
        await self.ledger.resolve([], ['derp'])
        self.assertEqual(50002, self.ledger.network.client.server[1])

    async def test_direct_sync(self):
        await self.ledger.stop()
        initial_height = self.ledger.local_height_including_downloaded_height
        await self.blockchain.generate(100)
        while self.conductor.spv_node.server.session_manager.notified_height < initial_height + 100:
            await asyncio.sleep(0.1)
        self.assertEqual(initial_height, self.ledger.local_height_including_downloaded_height)
        await self.ledger.headers.open()
        await self.ledger.network.start()
        await self.ledger.network.on_connected.first
        await self.ledger.initial_headers_sync()
        self.assertEqual(initial_height + 100, self.ledger.local_height_including_downloaded_height)

    async def test_connection_drop_still_receives_events_after_reconnected(self):
        address1 = await self.account.receiving.get_or_create_usable_address()
        # disconnect and send a new tx, should reconnect and get it
        self.ledger.network.client.transport.close()
        self.assertFalse(self.ledger.network.is_connected)
        await self.ledger.resolve([], ['derp'])
        sendtxid = await self.send_to_address_and_wait(address1, 1.1337, 1)
        self.assertLess(self.ledger.network.client.response_time, 1)  # response time properly set lower, we are fine

        await self.assertBalance(self.account, '1.1337')
        # is it real? are we rich!? let me see this tx...
        d = self.ledger.network.get_transaction(sendtxid)
        # what's that smoke on my ethernet cable? oh no!
        master_client = self.ledger.network.client
        self.ledger.network.client.connection_lost(Exception())
        with self.assertRaises(asyncio.TimeoutError):
            await d
        self.assertIsNone(master_client.response_time)  # response time unknown as it failed
        # rich but offline? no way, no water, let's retry
        with self.assertRaisesRegex(ConnectionError, 'connection is not available'):
            await self.ledger.network.get_transaction(sendtxid)
        # * goes to pick some water outside... * time passes by and another donation comes in
        sendtxid = await self.blockchain.send_to_address(address1, 42)
        await self.blockchain.generate(1)
        # (this is just so the test doesn't hang forever if it doesn't reconnect)
        if not self.ledger.network.is_connected:
            await asyncio.wait_for(self.ledger.network.on_connected.first, timeout=10.0)
        # omg, the burned cable still works! torba is fire proof!
        await self.ledger.on_header.where(self.blockchain.is_expected_block)
        await self.ledger.network.get_transaction(sendtxid)

    async def test_timeout_then_reconnect(self):
        # tests that it connects back after some failed attempts
        await self.conductor.spv_node.stop()
        self.assertFalse(self.ledger.network.is_connected)
        await asyncio.sleep(0.2)  # let it retry and fail once
        await self.conductor.spv_node.start(self.conductor.lbcwallet_node)
        await self.ledger.network.on_connected.first
        self.assertTrue(self.ledger.network.is_connected)

    async def test_timeout_and_concurrency_propagated_from_config(self):
        conf = Config()
        self.assertEqual(self.ledger.network.client.timeout, 30)
        self.assertEqual(self.ledger.network.client.concurrency, 32)
        conf.hub_timeout = 123.0
        conf.concurrent_hub_requests = 42
        conf.known_hubs = self.ledger.config['known_hubs']
        conf.wallet_dir = self.ledger.config['data_path']
        self.manager.config = conf
        await self.manager.reset()
        self.assertEqual(self.ledger.network.client.timeout, 123)
        self.assertEqual(self.ledger.network.client.concurrency, 42)

    # async def test_online_but_still_unavailable(self):
    #     # Edge case. See issue #2445 for context
    #     self.assertIsNotNone(self.ledger.network.session_pool.fastest_session)
    #     for session in self.ledger.network.session_pool.sessions:
    #         session.response_time = None
    #     self.assertIsNone(self.ledger.network.session_pool.fastest_session)


class UDPServerFailDiscoveryTest(AsyncioTestCase):
    async def test_wallet_connects_despite_lack_of_udp(self):
        conductor = Conductor()
        conductor.spv_node.udp_port = '0'
        await conductor.start_lbcd()
        self.addCleanup(conductor.stop_lbcd)
        await conductor.start_lbcwallet()
        self.addCleanup(conductor.stop_lbcwallet)
        await conductor.start_spv()
        self.addCleanup(conductor.stop_spv)
        self.assertFalse(conductor.spv_node.server.status_server.is_running)
        await asyncio.wait_for(conductor.start_wallet(), timeout=5)
        self.addCleanup(conductor.stop_wallet)
        self.assertTrue(conductor.wallet_node.ledger.network.is_connected)


class ServerPickingTestCase(AsyncioTestCase):
    async def _make_udp_server(self, port, latency) -> StatusServer:
        s = StatusServer()
        await s.start(0, b'\x00' * 32, 'US', '127.0.0.1', port, True)
        s.set_available()
        sendto = s._protocol.transport.sendto

        def mock_sendto(data, addr):
            self.loop.call_later(latency, sendto, data, addr)

        s._protocol.transport.sendto = mock_sendto

        self.addCleanup(s.stop)
        return s

    async def _make_fake_server(self, latency=1.0, port=1):
        # local fake server with artificial latency
        class FakeSession(RPCSession):
            async def handle_request(self, request):
                await asyncio.sleep(latency)
                if request.method == 'server.version':
                    return tuple(request.args)
                return {'height': 1}
        server = await self.loop.create_server(lambda: FakeSession(), host='127.0.0.1', port=port)
        self.addCleanup(server.close)
        await self._make_udp_server(port, latency)
        return '127.0.0.1', port

    async def _make_bad_server(self, port=42420):
        async def echo(reader, writer):
            while True:
                writer.write(await reader.read())

        server = await asyncio.start_server(echo, host='127.0.0.1', port=port)
        self.addCleanup(server.close)
        await self._make_udp_server(port, 0)
        return '127.0.0.1', port

    async def test_pick_fastest(self):
        ledger = Mock(config={
            'default_servers': [
                # fast but unhealthy, should be discarded
                # await self._make_bad_server(),
                ('localhost', 1),
                ('example.that.doesnt.resolve', 9000),
                await self._make_fake_server(latency=1.0, port=1340),
                await self._make_fake_server(latency=0.1, port=1337),
                await self._make_fake_server(latency=0.4, port=1339),
            ],
            'connect_timeout': 3
        })

        network = Network(ledger)
        self.addCleanup(network.stop)
        await network.start()
        await asyncio.wait_for(network.on_connected.first, timeout=10)
        self.assertTrue(network.is_connected)
        self.assertTupleEqual(network.client.server, ('127.0.0.1', 1337))
        # self.assertTrue(all([not session.is_closing() for session in network.session_pool.available_sessions]))
        # ensure we are connected to all of them after a while
        # await asyncio.sleep(1)
        # self.assertEqual(len(list(network.session_pool.available_sessions)), 3)
